实用的TypeScript Repositories:
TypeScript
类型体操姿势合集: https://github.com/type-challenges/type-challenges- 深入理解TypeScript(《TypeScript Deep Dive》的中文翻译版): https://github.com/jkchao/typescript-book-chinese
编译上下文算是一个比较花哨的术语,可以用它来给文件分组,告诉 TypeScript 哪些文件是有效的,哪些是无效的。除了有效文件所携带信息外,编译上下文还包含有正在被使用的编译选项的信息。定义这种逻辑分组,一个比较好的方式是使用 tsconfig.json
文件。
tsconfig文件是位于项目中的一个JSON文件。
好的 IDE 支持对 TypeScript 的即时编译。但是,如果你想在使用 tsconfig.json
时从命令行手动运行 TypeScript 编译器,你可以通过以下方式:
tsconfig.json
文件。tsc -p ./path-to-project-directory
。当然,这个路径可以是绝对路径,也可以是相对于当前目录的相对路径。你甚至可以使用 tsc -w
来启用 TypeScript 编译器的观测模式,在检测到文件改动之后,它将重新编译。
你也可以显式指定需要编译的文件:
你还可以使用 include
和 exclude
选项来指定需要包含的文件和排除的文件:
使用 globs
:**/*
(一个示例用法:some/folder/**/*
)意味着匹配所有的文件夹和所有文件(扩展名为 .ts/.tsx
,当开启了 allowJs: true
选项时,扩展名可以是 .js/.jsx
)。
在 TypeScript 里存在两种声明空间:类型声明空间与变量声明空间。
类型声明空间包含用来当做类型注解的内容,例如下面的类型声明:
你可以将 Foo
, Bar
, Bas
作为类型注解使用,示例如下:
注意,尽管你定义了 interface Bar
,却并不能够把它作为一个变量来使用,因为它没有定义在变量声明空间中。
出现错误提示: cannot find name 'Bar'
的原因是名称 Bar
并未定义在变量声明空间。这将带领我们进入下一个主题 -- 变量声明空间。
变量声明空间包含可用作变量的内容,在上文中 Class Foo
提供了一个类型 Foo
到类型声明空间,此外它同样提供了一个变量 Foo
到变量声明空间,如下所示:
这很棒,尤其是当你想把一个类来当做变量传递时。
我们并不能把一些如 interface
定义的内容当作变量使用。
与此相似,一些用 var
声明的变量,也只能在变量声明空间使用,不能用作类型注解。
提示 ERROR: "cannot find name 'foo'"
原因是,名称 foo 没有定义在类型声明空间里。
commonjs
, amd
, es modules
, others
首先,我们需要澄清这些模块系统的不一致性。我将会提供给你我当前的建议,以及消除一些你的顾虑。
你可以根据不同的 module
选项来把 TypeScript 编译成不同的 JavaScript 模块类型,这有一些你可以忽略的东西:
使用 module: commonjs
选项来替代这些模式,将会是一个好的主意。
怎么书写 TypeScript 模块呢?,这也是一件让人困惑的事。在今天我们应该这么做:
import/require
语法即 import foo = require('foo')
写法这很酷,接下来,让我们看看 ES 模块语法。
使用 module: commonjs
选项以及使用 ES 模块语法导入、导出、编写模块。
export
关键字导出一个变量或类型export
的写法除了上面这种,还有另外一种:import
关键字导入一个变量或者是一个类型:我并不喜欢用默认导出,虽然有默认导出的语法:
export default
let/const/var
);import someName from 'someModule'
语法(你可以根据需要为导入命名):如果你需要使用 moduleResolution: node
选项,你应该将此选项放入你的配置文件中。如果你使用了 module: commonjs
选项, moduleResolution: node
将会默认开启。
这里存在两种截然不同的模块:
.
开头,例如:./someFile
或者 ../../someFolder/someFile
等);core-js
,typestyle
,react
或者甚至是 react/core
等)。它们的主要区别在于系统如何解析模块。
我将会使用一个概念性术语,place
-- 将在提及查找模式后解释它。
这很简单,仅仅是按照相对路径来就可以了:
bar.ts
中含有 import * as foo from './foo'
,那么 foo
文件必须与 bar.ts
文件存在于相同的文件夹下bar.ts
中含有 import * as foo from '../foo'
,那么 foo
文件所存在的地方必须是 bar.ts
的上一级目录;bar.ts
中含有 import * as foo from '../someFolder/foo'
,那么 foo
文件所在的文件夹 someFolder
必须与 bar.ts
文件所在文件夹在相同的目录下。你还可以思考一下其他相对路径导入的场景。😃
当导入路径不是相对路径时,模块解析将会模仿 Node 模块解析策略,下面我将给出一个简单例子:
import * as foo from 'foo'
,将会按如下顺序查找模块:
./node_modules/foo
../node_modules/foo
../../node_modules/foo
import * as foo from 'something/foo'
,将会按照如下顺序查找内容
./node_modules/something/foo
../node_modules/something/foo
../../node_modules/something/foo
place
当我提及被检查的 place
时,我想表达的是在这个 place
上,TypeScript 将会检查以下内容(例如一个 foo
的 place
):
place
表示一个文件,如:foo.ts
,欢呼!place
是一个文件夹,并且存在一个文件 foo/index.ts
,欢呼!place
是一个文件夹,并且存在一个 foo/package.json
文件,在该文件中指定 types
的文件存在,那么就欢呼!place
是一个文件夹,并且存在一个 package.json
文件,在该文件中指定 main
的文件存在,那么就欢呼!从文件类型上来说,我实际上是指 .ts
, .d.ts
或者 .js
就是这样,现在你已经是一个模块查找专家(这并不是一个小小的成功)。
在你的项目里,你可以通过 declare module 'somePath'
声明一个全局模块的方式,来解决查找模块路径的问题。
接着 :
import/require
仅仅是导入类型以下导入语法:
它实际上只做了两件事:
你可以选择仅加载类型信息,而没有运行时的依赖关系。在继续之前,你可能需要重新阅读本书 声明空间部分 部分。
如果你没有把导入的名称当做变量声明空间来用,在编译成 JavaScript 时,导入的模块将会被完全移除。
类型推断需要提前完成,这意味着,如果你想在 bar
文件里,使用从其他文件 foo
导出的类型,你将不得不这么做:
然而,在某些情景下,你只想在需要时加载模块 foo
,此时你需要仅在类型注解中使用导入的模块名称,而不是在变量中使用。在编译成 JavaScript 时,这些将会被移除。接着,你可以手动导入你需要的模块。
作为一个例子,考虑以下基于 commonjs
的代码,我们仅在一个函数内导入 foo
模块:
一个同样简单的 amd
模块(使用 requirejs):
这些通常在以下情景使用:
类似于懒加载的使用用例,某些模块加载器(commonjs/node 和 amd/requirejs)不能很好的处理循环依赖。在这种情况下,一方面我们使用延迟加载代码,并在另一方面预先加载模块是很实用的。
当你加载一个模块,只是想引入其附加的作用(如:模块可能会注册一些像 CodeMirror addons)时,然而,如果你仅仅是 import/require
(导入)一些并没有与你的模块或者模块加载器有任何依赖的 JavaScript 代码,(如:webpack),经过 TypeScript 编译后,这些将会被完全忽视。在这种情况下,你可以使用一个 ensureImport
变量,来确保编译的 JavaScript 依赖与模块。如:
在上文中,当我们讨论文件模块时,比较了全局变量与文件模块,并且我们推荐使用基于文件的模块,而不是选择污染全局命名空间。
然而,如果你的团队里有 TypeScript 初学者,你可以提供他们一个 global.d.ts
文件,用来将一些接口或者类型放入全局命名空间里,这些定义的接口和类型能在你的所有 TypeScript 代码里使用。
TIP
对于任何需要编译成 JavaScript
的代码,我们强烈建议你放入文件模块里。
global.d.ts
是一种扩充 lib.d.ts
很好的方式,如果你需要的话。JS
迁移到 TS
时,定义 declare module "some-library-you-dont-care-to-get-defs-for"
能让你快速开始。在 JavaScript 使用命名空间时, 这有一个常用的、方便的语法:
something || (something = {})
允许匿名函数 function (something) {}
向现有对象添加内容,或者创建一个新对象,然后向该对象添加内容。这意味着你可以拥有两个由某些边界拆成的块。
在确保创建的变量不会泄漏至全局命名空间时,这种方式在 JavaScript 中很常见。当基于文件模块使用时,你无须担心这点,但是该模式仍然适用于一组函数的逻辑分组。因此 TypeScript 提供了 namespace
关键字来描述这种分组,如下所示。
namespace
关键字编译后的 JavaScript 代码,与我们早些时候看到的 JavaScript 代码一样。
值得注意的一点是,命名空间是支持嵌套的。因此,你可以做一些类似于在 Utility
命名空间下嵌套一个命名空间 Messaging
的事情。
对于大多数项目,我们建议使用外部模块和命名空间,来快速演示和移植旧的 JavaScript 代码。
动态导入表达式是 ECMAScript 的一个新功能,它允许你在程序的任意位置异步加载一个模块,TC39 JavaScript 委员会有一个提案,目前处于第四阶段,它被称为 import() proposal for JavaScript。
此外,webpack bundler 有一个 Code Splitting
功能,它能允许你将代码拆分为许多块,这些块在将来可被异步下载。因此,你可以在程序中首先提供一个最小的程序启动包,并在将来异步加载其他模块。
这很自然就会让人想到(如果我们工作在 webpack dev 的工作流程中)TypeScript 2.4 dynamic import expressions 将会把你最终生成的 JavaScript 代码自动分割成很多块。但是这似乎并不容易实现,因为它依赖于我们正在使用的 tsconfig.json
配置文件。
webpack 实现代码分割的方式有两种:使用 import()
(首选,ECMAScript 的提案)和 require.ensure()
(最后考虑,webpack 具体实现)。因此,我们期望 TypeScript 的输出是保留 import()
语句,而不是将其转化为其他任何代码。
让我们来看一个例子,在这个例子中,我们演示了如何配置 webpack 和 TypeScript 2.4 +。
在下面的代码中,我希望懒加载 moment
库,同时我也希望使用代码分割的功能,这意味 moment
会被分割到一个单独的 JavaScript 文件,当它被使用时,会被异步加载。
这是 tsconfig.json
的配置文件:
"module": "esnext"
选项:TypeScript 保留 import()
语句,该语句用于 Webpack Code Splitting。